/*
 *                 Sun Public License Notice
 * 
 * The contents of this file are subject to the Sun Public License
 * Version 1.0 (the "License"). You may not use this file except in
 * compliance with the License. A copy of the License is available at
 * http://www.sun.com/
 * 
 * The Original Code is NetBeans. The Initial Developer of the Original
 * Code is Sun Microsystems, Inc. Portions Copyright 1997-2006 Sun
 * Microsystems, Inc. All Rights Reserved.
 */
package actions;

import com.apple.eawt.*;
import java.awt.Component;

import javax.swing.*;
import java.awt.event.*;
import java.io.*;
import java.net.*;
import java.util.*;
import javax.swing.event.PopupMenuEvent;
import javax.swing.event.PopupMenuListener;

/**
 * Main entry point to the Actions library.  Given a resource bundle, an object 
 * to call methods on and an XML file, an
 * ActionManager can generate menus, toolbars, popup menus and can populate 
 * a component's input map/action map.
 *
 * @author Tim Boudreau
 */
public abstract class ActionManager {
    static final boolean log = false;
    ComponentRegistry ui = null;
    static final boolean SCREEN_MENU_BAR = (System.getProperty("mrj.version") != null) //NOI18N
        && ("true".equals(System.getProperty ("apple.awt.useScreenMenuBar"))); //NOI18N

    private final ActionController mgr;
    
    ActionManager(ActionController mgr) {
        this.mgr = mgr;
        if (mgr == null) {
            throw new NullPointerException ("State manager null");
        }
    }

    /** Create an instance of ActionManager configured from the passed XML file
     * and resource bundle, which will invoke methods on the passed Object.
     * The passed Object must have methods for all of the actions defined in
     * the XML file.
     */
    public static ActionManager createActionManager (URL xml, Object instance, ResourceBundle bundle) throws IOException {
        return new XMLActionManager (xml, instance, bundle);
    }
    
    public static ActionManager createActionManager (URL xml, Object instance, ResourceBundle bundle, ActionController mgr) throws IOException {
        return new XMLActionManager (xml, instance, bundle, mgr);
    }
    
    ActionController getState() {
        return mgr;
    }
    
    /**
     * A convenience utility method for getting an ActionStateManager with 
     * setters for enabling/disabling/selecting/deselecting actions.  Throws
     * a ClassCastException if you did not either pass an instance of 
     * SimpleActionController to the factory method use the factory method
     * that takes no such argument.
     */
    public final SimpleActionController getStateManager() {
        if (mgr instanceof SimpleActionController) {
            return ((SimpleActionController) mgr);
        } else {
            throw new ClassCastException ("A SimpleActionStateManager was not" +
                    " passed to the constructor");
        }
    }

    void appleInit() {
        if (System.getProperty("mrj.version") != null) { //NOI18N
            Set actions = getActions().keySet();
            if (actions.contains("about") || actions.contains ("exit") || //NOI18N
                    actions.contains ("options") ||
                    actions.contains ("preferences") || actions.contains("open")) { //NOI18N
                Application.getApplication().setEnabledAboutMenu(actions.contains("about")); //NOI18N
                Application.getApplication().setEnabledPreferencesMenu(
                        actions.contains ("options") || //NOI18N
                        actions.contains ("preferences")); //NOI18N

                Application.getApplication().addApplicationListener(new AppL());
            }
        }
    }

    private final class AppL extends ApplicationAdapter {
        public void handleAbout(ApplicationEvent event) {
            event.setHandled (perform ("about"));
        }

        public void handleOpenApplication(ApplicationEvent event) {
        }

        public void handleOpenFile(ApplicationEvent event) {
            event.setHandled (ActionManager.this.tryToOpen (event.getFilename ()));
        }

        public void handlePreferences(ApplicationEvent event) {
            boolean result = perform ("options"); //NOI18N
            if (!result) {
                result = perform ("preferences"); //NOI18N
            }
            event.setHandled (result);
        }

        public void handlePrintFile(ApplicationEvent event) {
            event.setHandled ((perform("print")));
        }

        public void handleQuit(ApplicationEvent event) {
            event.setHandled (perform ("exit")); //NOI18N
        }

        public void handleReOpenApplication(ApplicationEvent event) {
            //???
        }

        private boolean perform (String s) {
            Action a = getAction (s);
            boolean result = a != null;
            if (result) {
                ActionEvent e = new ActionEvent (this, ActionEvent.ACTION_PERFORMED, s);
                try {
                    a.actionPerformed (e);
                } catch (ActionFailedException ex) {
                    result = false;
                }
            }
            return result;
        }
    }

    abstract boolean tryToOpen (String filename);

    /**
     * Create a menu bar containing all of the menus defined in the XML file.
     * @return A menu bar suitable for use in a frame, with all actions
     *         configured to call the appropriate methods on the object
     *         instance passed to the constructor of this <code>ActionManager</code>
     */
    public final javax.swing.JMenuBar createMenuBar () {
        javax.swing.JMenuBar bar = new javax.swing.JMenuBar ();
        for ( java.util.Iterator i = getMenuOrder ().iterator (); i.hasNext (); ) {
            String menuid = (String) i.next ();
            if ( log ) log ( "Menu Bar Item: " + menuid );
            JMenu menu = createMenu ( menuid );
            if (menu.getMenuComponentCount() > 0) {
                //A help menu with one item, "About" should be suppressed on
                //Apple, where it's going to be on the app menu
                bar.add ( menu );
            }
        }
        return bar;
    }

    /**
     * Create a JButton configured with a named action defined in the XML file.
     * @param action The ID of the action from the XML file which should be
     *        invoked when the button is clicked
     */
    public final javax.swing.AbstractButton createButton (String action) {
        boolean togglable = mgr.isTogglable ( action );
        javax.swing.AbstractButton result = togglable ?
                (javax.swing.AbstractButton) new javax.swing.JToggleButton ( getAction ( action ) ) :
                (javax.swing.AbstractButton) new javax.swing.JButton ( getAction ( action ) );

        if (togglable) {
            ((JToggleButton) result).setSelected(false);
        }
        result.setName ( action );
        ui.registerComponent ( action, result, mgr.isEnabled(action), togglable ? mgr.isSelected(action) : false);
        configureMnemonic ( action, result );
        return result;
    }

    /**
     * Create an array of toolbars corresponding to those defined in the
     * XML file and populated with buttons corresponding to the relevant
     * actions.
     */
    public final javax.swing.JToolBar[] createToolbars () {
        javax.swing.JToolBar[] result = new javax.swing.JToolBar[ getToolbarOrder ().size () ];
        int idx = 0;
        boolean aqua = "Aqua".equals ( javax.swing.UIManager.getLookAndFeel ().getID () );
        for ( java.util.Iterator i = getToolbarOrder ().iterator (); i.hasNext (); ) {
            String toolbarId = (String) i.next ();
            javax.swing.JToolBar bar = new javax.swing.JToolBar ();
            if ( aqua ) { //Aqua uses basicToolbarUI - awful...
                 bar.setUI ( (javax.swing.plaf.ToolBarUI) actions.ui.AquaToolbarUI.createUI ( bar ) );
           }
            bar.setName ( toolbarId );
            java.util.List l = (java.util.List) getToolbars2actions ().get ( toolbarId );
            for ( java.util.Iterator j = l.iterator (); j.hasNext (); ) {
                String s = (String) j.next ();
                if ( s.startsWith (XMLActionsParser.SEPARATOR_PREFIX )) {
                    JSeparator sep = new JSeparator();
                    sep.setName ( s );
                    bar.add ( sep );
                } else {
                    javax.swing.Action a = getAction ( s );
                    javax.swing.AbstractButton button = mgr.isTogglable ( s ) ? 
                        (javax.swing.AbstractButton) new javax.swing.JToggleButton ( a ) : 
                        (javax.swing.AbstractButton) new javax.swing.JButton (
                            a );
                    button.setName (s);
                    if ( button.getIcon () != null ) {
                        button.setText ( "" );
                    } else {
                        configureMnemonic ( s, button );
                    }
                    ui.registerComponent ( s, button, mgr.isEnabled(s), mgr.isTogglable(s) ? mgr.isSelected(s) : false );
                    bar.add ( button );
                }
            }
            result[ idx ] = bar;
            idx++;
        }
        return result;
    }

    /**
     * Create a named <code>JPopupMenu</code> defined in a <code>popup</code> element
     * defined in the XML file.
     * @param context The ID of the popup menu to create
     */
    public final javax.swing.JPopupMenu createPopupMenu (String context) {
        javax.swing.JPopupMenu result = new javax.swing.JPopupMenu ();
        result.setName ( context );
        java.util.List l = (java.util.List) getPopups2actions ().get ( context );
        if ( l == null ) {
            throw new IllegalArgumentException ( "Non-existent popup menu id: " + context );
        }
        for ( java.util.Iterator j = l.iterator (); j.hasNext (); ) {
            String id = (String) j.next ();
            if ( id.startsWith ( XMLActionsParser.SEPARATOR_PREFIX )) {
                JSeparator sep = new JSeparator();
                sep.setName ( id );
                result.add ( sep );
            } else {
                javax.swing.Action action = getAction ( id );
                if ( log ) log ( "CREATE POPUP MENU ITEM ID " + id + " action " + action );
                javax.swing.AbstractButton comp;
                if ( action == null ) {
                    if ( log ) log ( "  creating submenu " + id );
                    comp = createMenu ( id );
                } else {
                    comp = mgr.isTogglable ( id ) ? new javax.swing.JCheckBoxMenuItem ( action ) : new javax.swing.JMenuItem ( action );
                    comp.setIcon ( null );
                    comp.setSelected(mgr.isSelected( id ));
                    if ( log ) log ( "  creating menu item " + comp );
                }
                comp.setName ( id );
                String s = comp.getText();
                int amp = actions.borrowed.Mnemonics.findMnemonicAmpersand(s);
                if (amp != -1) {
                    //Strip mnemonics for popup menus
                    if (amp == 0) {
                        s = s.substring(1);
                    } else {
                        s = s.substring(0, amp) + s.substring (amp+1, s.length());
                    }
                    comp.setText(s);
                }
                ui.registerComponent ( id, comp, mgr.isEnabled (id), mgr.isTogglable(id) ? mgr.isSelected(id) : false);
                //no mnemonics for popup menu items
                result.add ( comp );
            }
        }
        result.addPopupMenuListener(new PML());
        return result;
    }
    
    //Listener class which makes sure there is no memory leak - sets a different
    //action for all menu items.  This ensures our actions don't hold a reference
    //to the menu as a property change listener
    private static final class PML extends AbstractAction implements PopupMenuListener, Runnable {
        public void popupMenuWillBecomeVisible(PopupMenuEvent e) {
        }

        private JPopupMenu menu;
        public void popupMenuWillBecomeInvisible(PopupMenuEvent e) {
            menu = (JPopupMenu) e.getSource();
            //Must run it later or our dummy action will be invoked instead
            //of the real one
            SwingUtilities.invokeLater (this);
            menu.removePopupMenuListener(this);
        }
        
        public void run() {
            detachFrom (menu);
        }

        public void popupMenuCanceled(PopupMenuEvent e) {
        }
        
        private void detachFrom (JPopupMenu menu) {
            Component[] c = menu.getComponents();
            for (int i=0; i < c.length; i++) {
                if (c[i] instanceof AbstractButton) {
                    ((AbstractButton) c[i]).setAction (this);
                } else if (c[i] instanceof JMenu) {
                    detachFrom ((JMenu) c[i]);
                }
            }
        }
        
        private void detachFrom (JMenu menu) {
            Component[] c = menu.getComponents();
            for (int i=0; i < c.length; i++) {
                if (c[i] instanceof AbstractButton) {
                    ((AbstractButton) c[i]).setAction (this);
                } else if (c[i] instanceof JMenu) {
                    detachFrom ((JMenu) c[i]);
                }
            }
        }

        public void actionPerformed(ActionEvent e) {
        }
        
    }

    /**
     * Install keybindings defined in the XML file in the input map and
     * action map of the passed component.  Equivalent to calling 
     * <code>bindKeys ( jc.getInputMap(), jc.getActionMap() );</code>.
     */
    public final void bindKeys (JComponent jc) {
        bindKeys ( jc.getInputMap(), jc.getActionMap() );
    }

    /**
     * Populate an InputMap and ActionMap with keybindings defined in the
     * resource bundle passed to the constructor.
     */
    public final void bindKeys (javax.swing.InputMap keys, javax.swing.ActionMap actions) {
        for ( java.util.Iterator i = getKeystrokes2actions ().keySet ().iterator (); i.hasNext (); ) {
            javax.swing.KeyStroke stroke = (javax.swing.KeyStroke) i.next ();
            String id = (String) getKeystrokes2actions ().get ( stroke );
            javax.swing.Action a = getAction ( id );
            keys.put ( stroke, id );
            actions.put ( id, a );
        }
    }

    abstract String[] getMenuActions (String menuId);
    abstract String[] getToolbarActions (String toolbarId);
    abstract String[] getPopupActions (String menuId);
    abstract javax.swing.Action getAction (String id);
    abstract java.util.Map getActions ();

    private javax.swing.JMenu createMenu (String menuid) {
        assert menuid != null;
        javax.swing.JMenu menu;
        try {
            menu = new javax.swing.JMenu ( getBundle ().getString ( menuid ) );
        } catch ( java.util.MissingResourceException mre ) {
            if ( log ) log ( "No menu name: " + menuid );
            menu = new javax.swing.JMenu ( menuid );
        }
        configureMnemonic ( menuid, menu );
        menu.setName ( menuid );
        if ( log ) log ( "Created menu " + menuid );
        java.util.List l = (java.util.List) getMenus2actions ().get ( menuid );
        if ( l == null ) {
            l = (java.util.List) getSubmenus2actions ().get ( menuid );
        }
        if ( l == null ) {
            throw new IllegalArgumentException ( "Non-existent menu id: " + menuid );
        }
        for ( java.util.Iterator j = l.iterator (); j.hasNext (); ) {
            String id = (String) j.next ();
            if (SCREEN_MENU_BAR && ("preferences".equals(id) || "options".equals(id) || "exit".equals(id) || "about".equals(id))) {
                //Will be handled via ApplicationAdapter on mac
                continue;
            }
            if ( id.startsWith (XMLActionsParser.SEPARATOR_PREFIX )) {
                JSeparator sep = new JSeparator();
                sep.setName ( id );
                menu.add ( sep );
            } else {
                javax.swing.Action action = getAction ( id );
                if ( log ) log ( "CREATE MENU ITEM ID " + id + " action " + action );
                javax.swing.AbstractButton comp;
                if ( action == null ) {
                    if ( log ) log ( "  creating submenu " + id );
                    comp = createMenu ( id );
                } else {
                    comp = mgr.isTogglable ( id ) ? new javax.swing.JCheckBoxMenuItem ( action ) : new javax.swing.JMenuItem ( action );
                    comp.setIcon ( null );
                    if ( log ) log ( "  creating menu item " + comp );
                }
                comp.setName ( id );
                ui.registerComponent ( id, comp, mgr.isEnabled(id), mgr.isTogglable (id) ? mgr.isSelected(id) : false );
                configureMnemonic ( id, comp );
                menu.add ( comp );
            }
        }
        return menu;
    }

    private void configureMnemonic (String id, javax.swing.AbstractButton comp) {
        if ( comp instanceof javax.swing.JButton || comp instanceof javax.swing.JToggleButton ) {
            try {
                String tip = getBundle ().getString ( id + ".tooltip" );
                comp.setToolTipText ( tip );
            } catch ( java.util.MissingResourceException mre2 ) {
                if ( log ) log ( "no tooltip for " + id );
                comp.setToolTipText ( null );
            }
        }
        actions.borrowed.Mnemonics.setLocalizedText ( comp, comp.getText () );
        if (comp instanceof JMenuItem) {
            //Getting it from Action.DESCRIPTION
            comp.setToolTipText ("");
        }
    }

    static void log (String s) {
        XMLActionsParser.log ( s );
    }

    //XXX this is junk, get rid of it
    
    abstract java.util.ResourceBundle getBundle ();

    abstract java.util.Map getMenus2actions ();

    abstract java.util.Map getPopups2actions ();

    abstract java.util.Map getToolbars2actions ();

    abstract java.util.Map getKeystrokes2actions ();

    abstract java.util.Map getSubmenus2actions ();

    abstract java.util.List getMenuOrder ();

    abstract java.util.List getToolbarOrder ();

    abstract java.util.Map getSubmenuOrders ();
}
